// MazeView by David Bau
// Copyright 1994 by David Bau.  All rights reserved.
//
// I feel silly even saying this, but please don't charge for this
// module or any product containing a portion or modification of it,
// and when distributing, please give credit where credit is due.
//
// If you add anything to this module or derive anything interesting
// from it, please let me know!
//
// I'll probably be bau@cs.cornell.edu for a while.
//
// David Bau 01/21/94
// 777 South Avenue, Weston, MA 02193
//
// 01/13/94 Fixed panel maze so that it doesn't change the first
// time a main-view maze begins drawing; it only changes after the
// main-view maze finishes.  Also, fixed drawSelf to be more efficient.
// Removed a floaing-point problem with the exponential speed dial.
// Changed the default colors.
//
// 01/14/94 Eliminated lockFocus when [self canDraw]==NO
// 01/14/94 Modified by Richard Hess (rhess@consilium.com)
// Fixed startup problems when using MazeView with Engage! Desktop...
//
// 02/01/94 Changed maze generation algorithm to make trickier mazes,
// using a second RandomIntPicker.

#import <appkit/appkit.h>
#import <defaults/defaults.h>
#import <libc.h>
#import <time.h>
#import <dpsclient/wraps.h>
#import "MazeView.h"
#import "RandomIntPicker.h"
#import "Thinker.h"


static void ColorToString(NXColor color, char *str);
static NXColor ColorFromString(const char *str);
static NXColor RandomColor();

/* the kind of Maze view that appears in the control panel */
@implementation StaticMazeView

- initMaze
{
    /* frame the field 2 cells bigger than the view */
    ncols=MIN((((int)bounds.size.width-wallSize)/cellSize+2),MAXCOLS);
    nrows=MIN((((int)bounds.size.height-wallSize)/cellSize+2),MAXROWS);
    if (ncols<4) ncols=4;
    if (nrows<4) nrows=4;
    xoffset=((int)bounds.size.width-(ncols-2)*cellSize-wallSize)/2-cellSize;
    yoffset=((int)bounds.size.height-(nrows-2)*cellSize-wallSize)/2-cellSize;

    /* allocate grid */
    if (Grid) NX_ZONEREALLOC([self zone],Grid,mazecell,nrows*ncols);
    else NX_ZONEMALLOC([self zone],Grid,mazecell,nrows*ncols);

    [self computeMaze];

    /* choose a random starting place and direction */
    icur=(random()%(ncols-2)+1)+(random()%(nrows-2)+1)*ncols;
    dcur=random()%4;
    Grid[icur].bdir=4;
    ifirst=icur;
    Grid[icur].next=(-1);

    /* set the current colors */
    curWallColor=wallColor;
    curPathColor=pathColor;

    /* clear drawing buffers */
    wallRectListSize=0;
    pathRectListSize=0;

    /* quickly do half-of-a-maze-exploration */
    countDown=((nrows-2)*(ncols-2)*2-2)/2;
    while (countDown--) {
        int inew;
        inew=icur;
        switch (dcur) {
        case 0:
            if (!Grid[icur].eastwall) {dcur=0; inew=icur+1; break;}
        case 1:
            if (!Grid[icur].northwall) {dcur=1; inew=icur+ncols; break;}
        case 2:
            if (!Grid[icur].westwall) {dcur=2; inew=icur-1; break;}
        case 3:
            if (!Grid[icur].southwall) {dcur=3; inew=icur-ncols; break;}
            if (!Grid[icur].eastwall) {dcur=0; inew=icur+1; break;}
            if (!Grid[icur].northwall) {dcur=1; inew=icur+ncols; break;}
            if (!Grid[icur].westwall) {dcur=2; inew=icur-1; break;}
        }
        if (Grid[icur].bdir==dcur) {
            Grid[icur].bdir=(-1);
        } else {
            Grid[inew].next=ifirst;
            ifirst=inew;
            Grid[inew].bdir=(dcur+2)%4;
        }
        icur=inew;
        dcur=(dcur+3)%4;
    }

    return self;
}

- drawSelf:(const NXRect *)rects :(int)rectCount
{
    PSsetgray(NX_BLACK);
    if (rectCount>1) {
        NXRectFillList(rects+1,rectCount-1);
    } else {
        NXRectFill(rects);
    }
    return [super drawSelf:rects:rectCount];
}

/* main view can tell panel view what colors to use */
- setWallColor:(NXColor)wc pathColor:(NXColor)pc
{
    curWallColor=wallColor=wc;
    curPathColor=pathColor=pc;
    return self;
}

@end






@implementation MazeView

- initMaze
{
    int t;
    /* frame the field 2 cells bigger than the view */
    if (!cellSize) NXLogError("MazeView: cellSize is 0 in initMaze\n");
    ncols=MIN((((int)bounds.size.width-wallSize)/cellSize+2),MAXCOLS);
    nrows=MIN((((int)bounds.size.height-wallSize)/cellSize+2),MAXROWS);
    if (ncols<4) ncols=4;
    if (nrows<4) nrows=4;
    t=(int)bounds.size.width-(ncols-2)*cellSize-wallSize;
    xoffset=(t<0 ? t/2-cellSize : random()%(t+1)-cellSize);
    t=(int)bounds.size.height-(nrows-2)*cellSize-wallSize;
    yoffset=(t<0 ? t/2-cellSize : random()%(t+1)-cellSize);

    /* allocate grid */
    if (Grid) NX_ZONEREALLOC([self zone],Grid,mazecell,nrows*ncols);
    else NX_ZONEMALLOC([self zone],Grid,mazecell,nrows*ncols);
    [self computeMaze];
    countDown=(-1);
    ifirst=(-1);
    return self;
}

- computeMaze
{
    RandomIntPicker *picker1, *picker2;

    /* clear the "last" fields */
    [self clearMaze];

    /* compute the maze */
    icur=(random()%(ncols-2)+1)+(random()%(nrows-2)+1)*ncols;
    Grid[icur].bdir=4;
    Grid[icur].eastwall=Grid[icur].northwall=
        Grid[icur].westwall=Grid[icur].southwall=1;
    picker1=[[RandomIntPicker alloc] init];
    picker2=[[RandomIntPicker alloc] init];
    while (1) {
        [picker1 reset];
        [picker2 reset];
        if (Grid[icur+1].bdir<0) {
            [picker1 insert:0];
            if (Grid[icur+2].bdir<0) [picker2 insert:0];
        }
        if (Grid[icur+ncols].bdir<0) {
            [picker1 insert:1];
            if (Grid[icur+2*ncols].bdir<0) [picker2 insert:1];
        }
        if (Grid[icur-1].bdir<0) {
            [picker1 insert:2];
            if (Grid[icur-2].bdir<0) [picker2 insert:2];
        }
        if (Grid[icur-ncols].bdir<0) {
            [picker1 insert:3];
            if (Grid[icur-2*ncols].bdir<0) [picker2 insert:3];
        }
        if ([picker1 count]) {
            int choice;
            choice=[picker2 count] ? [picker2 choice] : [picker1 choice];
            switch(choice) {
            case 0: icur=icur+1; break;
            case 1: icur=icur+ncols; break;
            case 2: icur=icur-1; break;
            case 3: icur=icur-ncols; break;
            }
            Grid[icur].bdir=(choice+2)%4;
            Grid[icur].eastwall=Grid[icur].northwall=
                Grid[icur].westwall=Grid[icur].southwall=1;
        } else {
            switch (Grid[icur].bdir) {
            case 0: Grid[icur].eastwall=0; icur=icur+1;
                Grid[icur].westwall=0; break;
            case 1: Grid[icur].northwall=0; icur=icur+ncols;
                Grid[icur].southwall=0; break;
            case 2: Grid[icur].westwall=0; icur=icur-1;
                Grid[icur].eastwall=0; break;
            case 3: Grid[icur].southwall=0; icur=icur-ncols;
                Grid[icur].northwall=0; break;
            case 4: goto finish;
            }
        }
    }
 finish:
    [picker1 free];
    [picker2 free];

    /* walls are set; clear the "last" fields again */
    [self clearMaze];

    return self;
}

- firstStep
{
    /* choose a random starting place and direction */
    icur=(random()%(ncols-2)+1)+(random()%(nrows-2)+1)*ncols;
    dcur=random()%4;
    Grid[icur].bdir=4;
    ifirst=icur;
    Grid[icur].next=(-1);

    /* set the current colors */
    if (randomColor) {
        curWallColor=RandomColor();
        curPathColor=RandomColor();
    } else {
        curWallColor=wallColor;
        curPathColor=pathColor;
    }

    if (sharedInspectorPanel) {
        if (panelMazeView) {
            [panelMazeView setWallColor:curWallColor pathColor:curPathColor];
        }
        [sharedInspectorPanel display];
    }

    /* clear drawing buffers */
    wallRectListSize=0;
    pathRectListSize=0;

    /* clear drawing etc */
    PSsetgray(NX_BLACK);
    NXRectFill(&bounds);
    [self drawForward:icur];
    [self flushDrawing];

    /* start countDown */
    countDown=(nrows-2)*(ncols-2)*2-2;

    return self;
}

/* here's where the maze solving and drawing is done */
- oneStep
{
    int inew;
    BStimeval curtime;

    /* enforce the delay between frames */
    curtime=currentTimeInMs();
    if (curtime-lasttime<delay) {
        if (delay-(curtime-lasttime)>30) {
            usleep(15000);
            return self;
        }
        usleep((delay-(curtime-lasttime))*1000);
        curtime=currentTimeInMs();
    }
    lasttime=curtime;

    /* check if finished */
    if (countDown<=0) {
        /* do the following only when recycling, not when running 1st time */
        if (!countDown) {
            [self drawBack:icur];
            [self flushDrawing];
            NXPing();
            if (panelMazeView) {
                [panelMazeView initMaze];
            }
            [self initMaze];
        }
        [self firstStep];
        return self;
    }
    countDown--;

    /* follow-the-right-wall algorithm... switch fallthrough intentional */
    inew=icur;

    switch (dcur) {
    case 0:
        if (!Grid[icur].eastwall) {dcur=0; inew=icur+1; break;}
    case 1:
        if (!Grid[icur].northwall) {dcur=1; inew=icur+ncols; break;}
    case 2:
        if (!Grid[icur].westwall) {dcur=2; inew=icur-1; break;}
    case 3:
        if (!Grid[icur].southwall) {dcur=3; inew=icur-ncols; break;}
        if (!Grid[icur].eastwall) {dcur=0; inew=icur+1; break;}
        if (!Grid[icur].northwall) {dcur=1; inew=icur+ncols; break;}
        if (!Grid[icur].westwall) {dcur=2; inew=icur-1; break;}
    }

    if (Grid[icur].bdir==dcur) {
        [self drawBack:icur];
        Grid[icur].bdir=(-1);
    } else {
        Grid[inew].next=ifirst;
        ifirst=inew;
        Grid[inew].bdir=(dcur+2)%4;
        [self drawForward:inew];
    }

    [self flushDrawing];

    icur=inew;
    dcur=(dcur+3)%4;
    return self;
}


- drawForward:(int) index
{
    int x,y;

    x=(index%ncols)*cellSize+xoffset;
    y=(index/ncols)*cellSize+yoffset;
    switch (Grid[index].bdir) {
    case 0:
        [self addPathRectOrigin: x+wallSize+gapSize:y+wallSize+gapSize
                    size: cellSize:pathSize]; break;
    case 1:
        [self addPathRectOrigin: x+wallSize+gapSize:y+wallSize+gapSize
                    size: pathSize:cellSize]; break;
    case 2:
        [self addPathRectOrigin: x-gapSize:y+wallSize+gapSize
                    size: cellSize:pathSize]; break;
    case 3:
        [self addPathRectOrigin: x+wallSize+gapSize:y-gapSize
                    size: pathSize:cellSize]; break;
    case 4:
        [self addPathRectOrigin:x+wallSize+gapSize:y+wallSize+gapSize
              size: pathSize:pathSize]; break;
    }

    if (Grid[index].eastwall && Grid[index+1].next==(-2)) {
        [self addWallRectOrigin:x+cellSize:y size:wallSize:cellSize+wallSize];
    }
    if (Grid[index].northwall && Grid[index+ncols].next==(-2)) {
        [self addWallRectOrigin:x:y+cellSize size:cellSize+wallSize:wallSize];
    }
    if (Grid[index].westwall) {
        [self addWallRectOrigin:x:y size:wallSize:cellSize+wallSize];
    }
    if (Grid[index].southwall) {
        [self addWallRectOrigin:x:y size:cellSize+wallSize:wallSize];
    }

    return self;
}

- drawBack:(int) index
{
    int x,y;

    x=(index%ncols)*cellSize+xoffset;
    y=(index/ncols)*cellSize+yoffset;

    [self addEraseRectOrigin:x+wallSize+gapSize:y+wallSize+gapSize
          size:pathSize:pathSize];

    switch (Grid[index].bdir) {
    case 0:
        [self addEraseRectOrigin: x+wallSize+gapSize:y+wallSize+gapSize
                    size: cellSize:pathSize]; break;
    case 1:
        [self addEraseRectOrigin: x+wallSize+gapSize:y+wallSize+gapSize
                    size: pathSize:cellSize]; break;
    case 2:
        [self addEraseRectOrigin: x-gapSize:y+wallSize+gapSize
                    size: cellSize:pathSize]; break;
    case 3:
        [self addEraseRectOrigin: x+wallSize+gapSize:y-gapSize
                    size: pathSize:cellSize]; break;
    case 4:
        [self addEraseRectOrigin:x+wallSize+gapSize:y+wallSize+gapSize
              size: pathSize:pathSize]; break;
    }

    return self;
}

- addWallRectOrigin:(int) x :(int) y size:(int)w :(int) h
{
    wallRectList[wallRectListSize].origin.x=x;
    wallRectList[wallRectListSize].origin.y=y;
    wallRectList[wallRectListSize].size.height=h;
    wallRectList[wallRectListSize].size.width=w;
    if (++wallRectListSize>=RECTBUFSIZE) {
        [self flushWallRects];
    }
    return self;
}

- flushWallRects
{
    if (wallRectListSize) {
        NXSetColor(curWallColor);
        NXRectFillList(wallRectList, wallRectListSize);
        wallRectListSize=0;
    }
    return self;
}
    
- addPathRectOrigin:(int) x :(int) y size:(int)w :(int) h
{
    pathRectList[pathRectListSize].origin.x=x;
    pathRectList[pathRectListSize].origin.y=y;
    pathRectList[pathRectListSize].size.height=h;
    pathRectList[pathRectListSize].size.width=w;
    if (++pathRectListSize>=RECTBUFSIZE) {
        [self flushPathRects];
    }
    return self;
}

- flushPathRects
{
    if (pathRectListSize) {
        NXSetColor(curPathColor);
        NXRectFillList(pathRectList, pathRectListSize);
        pathRectListSize=0;
    }
    return self;
}

- addEraseRectOrigin:(int) x :(int) y size:(int)w :(int) h
{
    eraseRectList[eraseRectListSize].origin.x=x;
    eraseRectList[eraseRectListSize].origin.y=y;
    eraseRectList[eraseRectListSize].size.height=h;
    eraseRectList[eraseRectListSize].size.width=w;
    if (++eraseRectListSize>=RECTBUFSIZE) {
        [self flushEraseRects];
    }
    return self;
}

- flushEraseRects
{
    if (eraseRectListSize) {
        PSsetgray(NX_BLACK);
        NXRectFillList(eraseRectList, eraseRectListSize);
        eraseRectListSize=0;
    }
    return self;
}

- flushDrawing
{
    [self flushWallRects];
    [self flushPathRects];
    [self flushEraseRects];
    return self;
}
        
- drawSelf:(const NXRect *)rects :(int)rectCount
{
    int i;
    for (i=ifirst; i>=0; i=Grid[i].next) {
        int x,y;
        x=i%ncols; y=i/ncols;
        if (x*cellSize-gapSize+xoffset<rects[0].origin.x+rects[0].size.width &&
           x*cellSize+cellSize+wallSize+gapSize+xoffset>rects[0].origin.x &&
           y*cellSize-gapSize+yoffset<rects[0].origin.y+rects[0].size.height &&
           y*cellSize+cellSize+wallSize+gapSize+yoffset>rects[0].origin.y) {
            [self drawForward:i];
        }
    }
    [self flushDrawing];
    return self;
}

- (const char *) windowTitle
{	return "Maze";
}

- initFrame:(const NXRect *)frameRect
{
    Grid=NULL;
    [self getMazeDefaults];
    [super initFrame:frameRect];
    [self initMaze];
    return self;
}

- free
{
    if (Grid) NXZoneFree([self zone], Grid);
    Grid=NULL;
    return [super free];
}

static void ColorToString(NXColor color, char *str)
{
    sprintf(str,"%f %f %f",NXRedComponent(color),
            NXGreenComponent(color),NXBlueComponent(color));
}

static NXColor ColorFromString(const char *str)
{
    NXColor color;
    float r,g,b;
    sscanf(str,"%f %f %f",&r,&g,&b);
    r=(r<0.0 ? 0.0 : (r>1.0 ? 1.0 : r));
    g=(g<0.0 ? 0.0 : (g>1.0 ? 1.0 : g));
    b=(b<0.0 ? 0.0 : (b>1.0 ? 1.0 : b));
    color=NXConvertRGBToColor(r,g,b);
    return color;
}

static NXColor RandomColor()
{
    NXColor color;
    float r,g,b;
    r=((random()%65536)/65536.0)/2;
    g=((random()%65536)/65536.0)/2;
    b=((random()%65536)/65536.0)/2;
    color=NXConvertRGBToColor(r,g,b);
    return color;
}

- getMazeDefaults
{
    char c;

    static NXDefaultsVector MazeDefaults = {
        {"MazeFrameDelay",  "16"},
        {"MazeRandomColor", "No"},
        {"MazeWallColor",   "0.333338 0.066668 0.066668"},
        {"MazePathColor",   "0.200003 0.333338 0.466674"},
        {"MazeWallSize",   "2"},
        {"MazePathSize",   "4"},
        {"MazeGapSize",    "1"},
        {NULL}
    };
    
    NXRegisterDefaults([NXApp appName],MazeDefaults);
    sscanf(NXGetDefaultValue([NXApp appName],"MazeFrameDelay"),"%d",&delay);
    if (delay<0) delay=0;
    if (delay>MAXDELAY) delay=MAXDELAY;
    if ((c=*NXGetDefaultValue([NXApp appName],"MazeRandomColor"))=='y' ||
         c=='Y') {
        randomColor=YES;
    } else {
        randomColor=NO;
    }
    wallColor=
      ColorFromString(NXGetDefaultValue([NXApp appName],"MazeWallColor"));
    pathColor=
      ColorFromString(NXGetDefaultValue([NXApp appName],"MazePathColor"));
    sscanf(NXGetDefaultValue([NXApp appName],"MazeWallSize"),"%d",&wallSize);
    sscanf(NXGetDefaultValue([NXApp appName],"MazePathSize"),"%d",&pathSize);
    sscanf(NXGetDefaultValue([NXApp appName],"MazeGapSize"),"%d",&gapSize);
    cellSize=pathSize+wallSize+2*gapSize;
    if (cellSize<MINCELLSIZE) {
        pathSize+=MINCELLSIZE-cellSize;
        cellSize=MINCELLSIZE;
    }

    return self;
}


- sizeTo:(NXCoord)width :(NXCoord)height
{
    [super sizeTo:width :height];
    [self initMaze];
    return self;
}

- clearMaze
{
    int x,y;

    icur = -1;

    /* clear the field */
    for (y=0; y<nrows; y++) {
        for (x=0; x<ncols; x++) {
            if (x==0 || y==0 || x==ncols-1 || y==nrows-1) {
                Grid[x+y*ncols].bdir=4;
            } else {
                Grid[x+y*ncols].bdir=(-1);
            }
            Grid[x+y*ncols].next=(-2);
	}
    }

    return self;
}


/* quick update called when color has been changed */
- updateViews: sender
{
    /* update the panel view, if needed */
    if (sharedInspectorPanel) {
        if (panelMazeView) {
            [panelMazeView setWallColor:curWallColor pathColor:curPathColor];
        }
        [sharedInspectorPanel display];
    }

    /* update myself */
    if ([self canDraw]) {
        [self lockFocus];
        [self display];
        [self unlockFocus];
    }

    return self;
}

- inspector:sender
{
    char buf[MAXPATHLEN];
    if (!sharedInspectorPanel) {
        sprintf(buf,"%s/MazeInspector.nib",[sender moduleDirectory:"Maze"]);
        [NXApp loadNibFile:buf owner:self withNames:NO];
        /* initialize some of the panel objects... */
        if (panelWallColorWell) {
            [panelWallColorWell setColor:wallColor];
            [panelWallColorWell setEnabled:!randomColor];
        }
        if (panelPathColorWell) {
            [panelPathColorWell setColor:pathColor];
            [panelPathColorWell setEnabled:!randomColor];
        }
        if (randomColorSwitch) [randomColorSwitch setState:randomColor];
        if (panelSpeedSlider) {
            if (delay<1) {
                [panelSpeedSlider setFloatValue:1.0];
            } else {
                [panelSpeedSlider setFloatValue:
                 (float)(-log(delay/99.0)/log(100.0))];
            }
        }
    }
    return sharedInspectorPanel;
}

- inspectorInstalled
{
    [self hideCredits:self];
    if (sharedInspectorPanel) {
        [sharedInspectorPanel display];
    }
    return self;
}

- doRandomColorSwitch:sender
{
    if ([sender state]) {
        [panelWallColorWell setEnabled:NO];
        [panelPathColorWell setEnabled:NO];
        randomColor=YES;
        curWallColor=RandomColor();
        curPathColor=RandomColor();
        NXWriteDefault([NXApp appName],"MazeRandomColor","Yes");
    } else {
        [panelWallColorWell setEnabled:YES];
        [panelPathColorWell setEnabled:YES];
        randomColor=NO;
        curWallColor=wallColor;
        curPathColor=pathColor;
        NXWriteDefault([NXApp appName],"MazeRandomColor","No");
    }
    [self perform:@selector(updateViews:) with:sender
          afterDelay:0 cancelPrevious:YES];
    return self;
}

- doSpeedSlider:sender
{
    char str[80];
    delay=99.0*exp(-[sender floatValue]*log(100.0));
    sprintf(str,"%d",delay);
    NXWriteDefault([NXApp appName],"MazeFrameDelay",str);
    return self;
}

- takeWallColorFrom:sender
{
    char str[80];
    curWallColor=wallColor=[(NXColorWell *)sender color];
    [self perform:@selector(updateViews:) with:sender
          afterDelay:0 cancelPrevious:YES];
    ColorToString(wallColor,str);
    NXWriteDefault([NXApp appName],"MazeWallColor",str);
    return self;
}

- takePathColorFrom:sender
{
    char str[80];
    curPathColor=pathColor=[(NXColorWell *)sender color];
    [self perform:@selector(updateViews:) with:sender
          afterDelay:0 cancelPrevious:YES];
    ColorToString(pathColor,str);
    NXWriteDefault([NXApp appName],"MazePathColor",str);
    return self;
}

- showCredits:sender
{
    if (panelCreditsView && sharedInspectorPanel) {
        if ([panelCreditsView isDescendantOf:sharedInspectorPanel]) {
            [panelCreditsView removeFromSuperview];
        }
        [sharedInspectorPanel addSubview:panelCreditsView];
        [sharedInspectorPanel display];
    }
    return self;
}

- hideCredits:sender
{
    if (panelCreditsView && sharedInspectorPanel) {
        if ([panelCreditsView isDescendantOf:sharedInspectorPanel]) {
            [panelCreditsView removeFromSuperview];
        }
        [sharedInspectorPanel display];
    }
    return self;
}

/* these methods are needed because we should not rearrange the view */
/* hierarchy while a button is active; we queue the rearranging to be */
/* done later, when nothing is locked on the view */

- doShowCredits:sender
{
    [self perform:@selector(showCredits:)
          with:self afterDelay:0 cancelPrevious:YES];
    return self;
}

- doHideCredits:sender
{
    [self perform:@selector(hideCredits:)
          with:self afterDelay:0 cancelPrevious:YES];
    return self;
}

@end


